package org.krayne.swivel.forms;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.ArrayList;
import java.util.Collection;

import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;

import org.krayne.swivel.container.PackContainer;
import org.krayne.swivel.container.PackDirection;
import org.krayne.swivel.container.TitleContainer;

/**
 * An {@code EasyFormPanel} makes it, well, easy to make a simple form.
 * You can easily add various form fields, as long as each is a
 * {@code java.awt.Component}. Each form field can have optional trailing
 * text. This can be useful for units strings, for example. You can also add 
 * headings for different sections of the form.
 * 
 * @author dhsu
 */
public class EasyFormPanel extends TitleContainer {
    private static final long serialVersionUID = 6170841730686570124L;
    
    private static final int SCROLL_INCREMENT = 15;
    private static final int DEFAULT_TITLE_PADDING = 13;
    private static final int FORM_PADDING = 5;
    private static final Insets FORM_FIELD_INSETS = new Insets(5, 5, 5, 5);
    private static final Insets HEADER_INSETS = new Insets(15, 1, 5, 1);
    
    // # of columns for the grid bag layout. 1 for field name, 1 for the field
    private static final int NUM_COLUMNS = 2;
    
    // the main panel that will contain all the form elements
    private final JPanel mainFormPanel;
    
    // the last grid y for the grid bag layout
    private int lastGridBagY;

    // grid bag constraints for the different form elements
    private GridBagConstraints headingConstraints;
    private GridBagConstraints fieldNameConstraints;
    private GridBagConstraints componentConstraints;
    private GridBagConstraints extraTextConstraints;
    
    // label and heading stores so we can change fonts easily
    private final Collection<EasyFormHeading> headings;
    private final Collection<JLabel> fieldNameLabels;
    private final Collection<JLabel> trailingTextLabels;
    
    /**
     * Constructs an empty {@code EasyFormPanel} with no title bar.
     */
    public EasyFormPanel() {
        this(null);
    }

    /**
     * Constructs an empty {@code EasyFormPanel} with the specified title.
     * 
     * @param formTitle the form title
     */
    public EasyFormPanel(String formTitle) {
        super(null, formTitle, false);
        this.setTitleBarVisible(formTitle != null);
        this.setTitleBarPadding(DEFAULT_TITLE_PADDING);
        
        this.headings = new ArrayList<EasyFormHeading>();
        this.fieldNameLabels = new ArrayList<JLabel>();
        this.trailingTextLabels = new ArrayList<JLabel>();

        this.lastGridBagY = 0;
        
        // initialize constraints
        this.headingConstraints = new GridBagConstraints();
        this.headingConstraints.gridwidth = NUM_COLUMNS;
        this.headingConstraints.weightx = 1.0;
        this.headingConstraints.anchor = GridBagConstraints.NORTHWEST;
        this.headingConstraints.fill = GridBagConstraints.HORIZONTAL;
        this.headingConstraints.insets = HEADER_INSETS;
        this.headingConstraints.gridx = 0;
        this.fieldNameConstraints = new GridBagConstraints();
        this.fieldNameConstraints.weightx = 0.0;
        this.fieldNameConstraints.anchor = GridBagConstraints.NORTHEAST;
        this.fieldNameConstraints.gridx = 0;
        this.fieldNameConstraints.insets = FORM_FIELD_INSETS;
        this.componentConstraints = new GridBagConstraints();
        this.componentConstraints.weightx = 0.5;
        this.componentConstraints.anchor = GridBagConstraints.NORTHWEST;
        this.componentConstraints.gridx = 1;
        this.componentConstraints.insets = FORM_FIELD_INSETS;
        this.extraTextConstraints = new GridBagConstraints();
        this.extraTextConstraints.weightx = 0.5;
        this.extraTextConstraints.anchor = GridBagConstraints.NORTHEAST;
        this.extraTextConstraints.gridx = 2;
        this.extraTextConstraints.insets = FORM_FIELD_INSETS;
        
        this.mainFormPanel = new JPanel();
        this.mainFormPanel.setLayout(new GridBagLayout());
        this.mainFormPanel.setBorder(BorderFactory.createEmptyBorder(
                FORM_PADDING, FORM_PADDING, FORM_PADDING, FORM_PADDING));

        Component packedMainFormPanel = new PackContainer(mainFormPanel, PackDirection.TOP);
        JScrollPane mainFormScrollPane = new JScrollPane(packedMainFormPanel);
        mainFormScrollPane.getVerticalScrollBar().setUnitIncrement(SCROLL_INCREMENT);
        this.setComponent(mainFormScrollPane);
    }

    /**
     * Sets the font for all section headings on this form.
     * 
     * @param font the heading font
     */
    public void setHeadingFont(Font font) {
        for(EasyFormHeading heading : headings) {
            heading.setFont(font);
        }
    }
    
    /**
     * Sets the font for all field names as well as for field trailing text.
     * 
     * @param font field name and trailing text font
     */
    public void setFieldNameFont(Font font) {
        for(JLabel label : fieldNameLabels) {
            label.setFont(font);
        }
        for(JLabel label : trailingTextLabels) {
            label.setFont(font);
        }
    }
    
    /**
     * Adds a section heading to the form.
     * 
     * @param headingString the heading name
     */
    public void addHeading(String headingString) {
        headingConstraints.gridy = lastGridBagY++;
        EasyFormHeading heading = new EasyFormHeading(headingString);
        mainFormPanel.add(heading, headingConstraints);
        headings.add(heading);
    }
    
    /**
     * Adds a field that dynamically resizes with the size of the form.
     * 
     * @param fieldName the field name (can be null for no name)
     * @param component the field component
     * @param minWidth the minimum width of the component (the component will
     *                 not shrink beyond this minimum width)
     */
    public void addDynamicWidthField(String fieldName, Component component, int minWidth) {
        this.addDynamicWidthField(fieldName, component, minWidth, null);
    }
    
    /**
     * Adds a field with trailing text that dynamically resizes with the size 
     * of the form.
     * 
     * @param fieldName the field name (can be null for no name)
     * @param component the field component
     * @param minWidth the minimum width of the component (the component will
     *                 not shrink beyond this minimum width)
     * @param trailingText text placed to the right of the field component
     */
    public void addDynamicWidthField(String fieldName, Component component, int minWidth, String trailingText) {
        this.addFieldNameLabel(fieldName);
        this.componentConstraints.gridy = lastGridBagY;
        this.componentConstraints.fill = GridBagConstraints.HORIZONTAL;
        this.addFieldComponent(component, minWidth, trailingText, componentConstraints);
    }
    
    /**
     * Adds a fixed-width field to the form.
     * 
     * @param fieldName the field name (can be null for no name)
     * @param component the field component
     * @param width the fixed width of the component
     */
    public void addFixedWidthField(String fieldName, Component component, int width) {
        this.addFixedWidthField(fieldName, component, width, null);
    }
    
    /**
     * Adds a fixed-width field that has trailing text to the form.
     * 
     * @param fieldName the field name (can be null for no name)
     * @param component the field component
     * @param width the fixed width of the component
     * @param trailingText text placed to the right of the field component
     */
    public void addFixedWidthField(String fieldName, Component component, int width, String trailingText) {
        this.addFieldNameLabel(fieldName);
        this.componentConstraints.gridy = lastGridBagY;
        this.componentConstraints.fill = GridBagConstraints.NONE;
        this.addFieldComponent(component, width, trailingText, componentConstraints);
    }
    
    /**
     * Adds a field name to the form at the current grid y position, based on
     * {@code this.lastGridBagY}.
     * 
     * @param fieldName the field name
     */
    private void addFieldNameLabel(String fieldName) {
        // create and add field name label
        fieldName = (fieldName == null) ? "" : fieldName;
        JLabel fieldNameLabel = new JLabel(fieldName);
        this.fieldNameLabels.add(fieldNameLabel);
        this.fieldNameConstraints.gridy = lastGridBagY;
        this.mainFormPanel.add(fieldNameLabel, fieldNameConstraints);
    }
    
    /**
     * Adds a field component to the form at the current grid y position, based
     * on {@code this.lastGridBagY}.
     * 
     * @param component the field component
     * @param width the width (or minimum width depending on constraints) of
     *              the component, which will set the preferred width of the
     *              component
     * @param trailingText text placed to the right of the field component
     * @param constraints the {@code GridBagConstraints} to use
     */
    private void addFieldComponent(Component component, int width, String trailingText, GridBagConstraints constraints) {
        int height = (int) component.getPreferredSize().getHeight();
        component.setPreferredSize(new Dimension(width, height));
        if(trailingText != null && trailingText.trim().length() > 0) {
            TrailingTextPanel trailingTextPanel = new TrailingTextPanel(component, trailingText);
            this.trailingTextLabels.add(trailingTextPanel.getTrailingTextLabel());
            this.mainFormPanel.add(trailingTextPanel, constraints);
        } else {
            this.mainFormPanel.add(component, constraints);
        }
        
        lastGridBagY++;
    }
    
    //--------------------------------------------------------------------------
    
    /**
     * A simple container for the specified component that adds text to the
     * right side.
     */
    private static class TrailingTextPanel extends JPanel {
        private static final long serialVersionUID = -5596504202076672699L;
        private static final int TRAILING_TEXT_SPACING = 5;
        private JLabel trailingTextLabel;
        
        /**
         * Constructs a {@code TrailingTextPanel} using the specified component
         * and text.
         * 
         * @param component the main component
         * @param trailingText the trailing text
         */
        public TrailingTextPanel(Component component, String trailingText) {
            super(new BorderLayout());
            this.trailingTextLabel = new JLabel(trailingText);
            this.trailingTextLabel.setBorder(new EmptyBorder(0, TRAILING_TEXT_SPACING, 0, 0));
            this.trailingTextLabel.setVerticalAlignment(SwingConstants.TOP);
            
            // layout
            this.add(component, BorderLayout.CENTER);
            this.add(trailingTextLabel, BorderLayout.EAST);
        }
        
        /**
         * Gets the trailing text label.
         * 
         * @return the trailing text label
         */
        public JLabel getTrailingTextLabel() {
            return this.trailingTextLabel;
        }
    }
    
    //--------------------------------------------------------------------------
    
    /**
     * A simple section heading component.
     */
    private static class EasyFormHeading extends JPanel {
        private static final long serialVersionUID = -555115082223953542L;
        private final JLabel headingLabel;
        private final JLabel emptyLabel;
        
        /**
         * Constructs a section heading with the specified string.
         * 
         * @param heading the heading string
         */
        public EasyFormHeading(String heading) {
            super(new BorderLayout());
            this.headingLabel = new JLabel(heading);
            this.emptyLabel = new JLabel();
            this.emptyLabel.setBorder(new EtchedBorder());
            
            this.add(headingLabel, BorderLayout.NORTH);
            this.add(emptyLabel, BorderLayout.CENTER);
        }
        
        /**
         * {@inheritDoc}
         */
        public void setFont(Font font) {
            super.setFont(font);
            if(headingLabel != null) {
                headingLabel.setFont(font);
            }
        }
    }
}
